Skip to content

Move worktree bootstrap to the server and persist terminal launch context#1518

Open
juliusmarminge wants to merge 6 commits intomainfrom
t3code/persist-script-terminals
Open

Move worktree bootstrap to the server and persist terminal launch context#1518
juliusmarminge wants to merge 6 commits intomainfrom
t3code/persist-script-terminals

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Mar 29, 2026

Summary

  • Persist the terminal launch context so newly opened terminals keep the worktree cwd and env during first-send worktree setup flows.
  • Update draft-thread worktree creation to flush state immediately, so the terminal drawer opens with the correct worktree context instead of briefly using the project root.
  • Add a regression test covering local draft threads, worktree setup scripts, and terminal open/write requests.

Note

Medium Risk
Moderate risk because it changes the thread.turn.start command flow to optionally create threads/worktrees and run setup scripts server-side, plus adds new terminal state/launch-context handling that could affect terminal routing and onboarding flows.

Overview
Moves worktree/thread bootstrap off the client and into the server. thread.turn.start now supports an optional bootstrap payload so the server can (optionally) create the thread, create a git worktree, update thread metadata, and then dispatch the actual turn.

Introduces server-driven setup-script execution. Adds a new ProjectSetupScriptRunner service/layer that opens a deterministic setup terminal with T3CODE_PROJECT_ROOT/T3CODE_WORKTREE_PATH env and writes the setup command; GitManager.preparePullRequestThread can now trigger it for new PR worktrees when a threadId is provided (failures are logged and do not block worktree creation).

Improves terminal UX by persisting launch context. The web app adds terminalLaunchContextByThreadId and an ensureTerminal action; terminal start/restart events seed launch context so the terminal drawer uses the correct cwd/worktree context during bootstrap/setup, and client-side PR/worktree setup execution is removed in favor of server ownership.

Contracts and tests updated accordingly. Adds schema support for bootstrap on thread.turn.start, extends git.preparePullRequestThread input with optional threadId, shares project-script helpers via @t3tools/shared/projectScripts, and adds coverage for PR worktree setup, bootstrap turn-starts, and terminal context behavior.

Written by Cursor Bugbot for commit e3372a4. This will update automatically on new commits. Configure here.

Note

Move worktree bootstrap and setup script execution to the server on thread turn start

  • The client no longer performs git worktree creation or setup script execution on first send; instead, thread.turn.start commands can carry an optional bootstrap payload (createThread, prepareWorktree, runSetupScript) handled by the server in wsServer.ts.
  • A new ProjectSetupScriptRunner service (ProjectSetupScriptRunner.ts) opens a terminal with the correct working directory and environment variables and writes the setup command, returning structured status metadata.
  • GitManager.preparePullRequestThread now invokes ProjectSetupScriptRunner.runForThread after creating a new worktree when a threadId is provided; failures are swallowed with a warning and do not fail the operation.
  • The terminal state store gains ensureTerminal, setTerminalLaunchContext, and clearTerminalLaunchContext to track per-thread terminal launch context; the UI derives terminal drawer cwd and worktreePath from this context.
  • Shared project script utilities (projectScriptCwd, projectScriptRuntimeEnv, setupProjectScript) are extracted to packages/shared and reused by both client and server.
  • Risk: bootstrap error handling in dispatchBootstrapTurnStart attempts thread cleanup on failure, but partial state (e.g. a created worktree without a completed turn) may persist if cleanup itself fails.

Macroscope summarized e3372a4.

- keep terminal drawer attached to the created worktree for first-send draft setup scripts
- flush local thread and draft state updates before opening terminals
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 02670574-534e-4813-8f1e-54e268b113f4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/persist-script-terminals

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 29, 2026
- launch project setup scripts from the server after worktree prep
- keep setup terminal state tied to the thread and surface activity events
- add coverage for setup runner and bootstrap turn flow
@github-actions github-actions bot added size:XL 500-999 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Mar 29, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale "preparing-worktree" phase shows misleading UI state
    • Removed the stale 'preparing-worktree' send phase, its derived isPreparingWorktree flag, and all UI references since worktree preparation now happens server-side and the phase no longer corresponds to any client-side work.

Create PR

Or push these changes by commenting:

@cursor push 88bafdb9f0
Preview (88bafdb9f0)
diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts
--- a/apps/web/src/components/ChatView.logic.ts
+++ b/apps/web/src/components/ChatView.logic.ts
@@ -75,7 +75,7 @@
   return previewUrls;
 }
 
-export type SendPhase = "idle" | "preparing-worktree" | "sending-turn";
+export type SendPhase = "idle" | "sending-turn";
 
 export interface PullRequestDialogState {
   initialReference: string | null;

diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -658,7 +658,6 @@
   const selectedModelForPicker = selectedModel;
   const phase = derivePhase(activeThread?.session ?? null);
   const isSendBusy = sendPhase !== "idle";
-  const isPreparingWorktree = sendPhase === "preparing-worktree";
   const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint;
   const nowIso = new Date(nowTick).toISOString();
   const activeWorkStartedAt = deriveActiveWorkStartedAt(
@@ -2573,7 +2572,7 @@
     }
 
     sendInFlightRef.current = true;
-    beginSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn");
+    beginSendPhase("sending-turn");
 
     const composerImagesSnapshot = [...composerImages];
     const composerTerminalContextsSnapshot = [...sendableComposerTerminalContexts];
@@ -4006,11 +4005,6 @@
                         {activeContextWindow ? (
                           <ContextWindowMeter usage={activeContextWindow} />
                         ) : null}
-                        {isPreparingWorktree ? (
-                          <span className="text-muted-foreground/70 text-xs">
-                            Preparing worktree...
-                          </span>
-                        ) : null}
                         {activePendingProgress ? (
                           <div className="flex items-center gap-2">
                             {activePendingProgress.questionIndex > 0 ? (
@@ -4115,11 +4109,9 @@
                               aria-label={
                                 isConnecting
                                   ? "Connecting"
-                                  : isPreparingWorktree
-                                    ? "Preparing worktree"
-                                    : isSendBusy
-                                      ? "Sending"
-                                      : "Send message"
+                                  : isSendBusy
+                                    ? "Sending"
+                                    : "Send message"
                               }
                             >
                               {isConnecting || isSendBusy ? (

juliusmarminge and others added 2 commits March 30, 2026 10:24
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
return yield* orchestrationEngine.dispatch(finalTurnStartCommand);
}).pipe(Effect.mapError(toBootstrapRouteRequestError));

return yield* bootstrapProgram.pipe(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low src/wsServer.ts:787

When a defect (Die) occurs inside bootstrapProgram (e.g., a null dereference in git.createWorktree), Effect.catch fails to intercept it because Effect.catch only catches typed Fail errors via Cause.findError. The cleanupCreatedThread() handler never runs, leaving orphaned threads. Consider using Effect.catchCause to handle all failure modes including defects.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/wsServer.ts around line 787:

When a defect (`Die`) occurs inside `bootstrapProgram` (e.g., a null dereference in `git.createWorktree`), `Effect.catch` fails to intercept it because `Effect.catch` only catches typed `Fail` errors via `Cause.findError`. The `cleanupCreatedThread()` handler never runs, leaving orphaned threads. Consider using `Effect.catchCause` to handle all failure modes including defects.

Evidence trail:
1. apps/server/src/wsServer.ts lines 787-791: `Effect.catch((error) => cleanupCreatedThread().pipe(...))` - the code under review
2. apps/server/src/wsServer.ts lines 740-785: `bootstrapProgram` definition showing calls to `git.createWorktree()` and other operations that could throw defects
3. apps/server/src/wsServer.ts lines 675-685: `cleanupCreatedThread()` function definition showing it uses `Effect.ignoreCause({ log: true })` but depends on being called
4. https://github.com/PaulJPhilp/EffectPatterns - Effect Patterns Hub documents 'Handle Unexpected Errors by Inspecting the Cause | Use Effect.catchAllCause or Effect.runFork to inspect the Cause of a failure, distinguishing between expected errors (Fail) and unexpected defects (Die)'
5. Effect library version: effect 4.0.0-beta.42 (package.json in root)

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Terminal event handler opens drawer for all terminal events
    • Guarded the handler to only fire for setup terminals (IDs starting with 'setup-') and simplified worktreePath to always use event.snapshot.cwd since non-setup terminals are now excluded.

Create PR

Or push these changes by commenting:

@cursor push b28aa51f71
Preview (b28aa51f71)
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -223,12 +223,15 @@
       domainEventFlushThrottler.maybeExecute();
     });
     const unsubTerminalEvent = api.terminal.onEvent((event) => {
-      if (event.type === "started" || event.type === "restarted") {
+      if (
+        (event.type === "started" || event.type === "restarted") &&
+        event.terminalId.startsWith("setup-")
+      ) {
         const threadId = ThreadId.makeUnsafe(event.threadId);
         ensureTerminal(threadId, event.terminalId, { open: true, active: true });
         setTerminalLaunchContext(threadId, {
           cwd: event.snapshot.cwd,
-          worktreePath: event.terminalId.startsWith("setup-") ? event.snapshot.cwd : null,
+          worktreePath: event.snapshot.cwd,
         });
       }
       const hasRunningSubprocess = terminalRunningSubprocessFromEvent(event);

You can send follow-ups to this agent here.

Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge changed the title Persist terminal launch context for worktree drafts Move worktree bootstrap to the server and persist terminal launch context Mar 30, 2026
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Preparing-worktree indicator set and immediately reset
    • Moved beginLocalDispatch({ preparingWorktree: false }) from before dispatchCommand to after it, so the preparing-worktree indicator stays true during the server-side worktree creation.

Create PR

Or push these changes by commenting:

@cursor push c62a5d68cc
Preview (c62a5d68cc)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -2803,7 +2803,6 @@
         });
       }
 
-      beginLocalDispatch({ preparingWorktree: false });
       const turnAttachments = await turnAttachmentsPromise;
       const bootstrap =
         isLocalDraftThread || baseBranchForWorktree
@@ -2851,6 +2850,7 @@
         ...(bootstrap ? { bootstrap } : {}),
         createdAt: messageCreatedAt,
       });
+      beginLocalDispatch({ preparingWorktree: false });
       turnStartSucceeded = true;
     })().catch(async (err: unknown) => {
       if (

You can send follow-ups to this agent here.

@juliusmarminge
Copy link
Copy Markdown
Member Author

@cursor push c62a5d6

…renders during server-side worktree creation

Applied via @cursor push command
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants